package project.svc.generic.db;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import online.model.ModelUtil;
import online.model.UniModel;

import org.apache.logging.log4j.LogManager;

import project.base.UpdateAbstract;

import common.db.JdbcSource;
import common.db.dao.Dao;
import common.db.dao.DaoConstraintException;
import common.db.dao.DaoLockException;
import common.db.dao.DaoUtil;
import common.db.dao.hibernate.HibernateJdbcWork;

import core.config.Factory;
import core.util.ArrayUtil;
import core.util.NumberUtil;
import core.util.bean.CamelCase;

/**
 * 汎用レコード操作
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class RecordOperate extends UpdateAbstract {

	/** テーブル名 */
	private EntityInfo mm;
	/** 論理削除フラグ */
	private Boolean invisible = null;

	/**
	 * テーブル名設定
	 *
	 * @param val テーブル名
	 */
	public void setTable(final String val) {
		this.mm = new EntityInfo(val);
		super.setCreatorName(this.mm.getCreatorName());
		super.setCreatedName(this.mm.getCreatedName());
		super.setUpdaterName(this.mm.getUpdaterName());
		super.setUpdatedName(this.mm.getUpdatedName());
		super.setVersionName(this.mm.getVersionName());
	}

	/**
	 * 管理項目判断
	 * @param name 項目名
	 * @return 管理項目の場合 true を返す。
	 */
	public boolean isMaintenance(final String name) {
		return this.mm.getVersionName().equals(name)
				|| this.mm.getCreatorName().equals(name)
				|| this.mm.getCreatedName().equals(name)
				|| this.mm.getUpdaterName().equals(name)
				|| this.mm.getUpdatedName().equals(name);
	}

	/**
	 * 論理削除設定
	 * @param val 論理削除フラグ
	 */
	public void setLogicalDelte(final boolean val) {
		this.invisible = Boolean.valueOf(val);
	}

	/**
	 * 論理削除判断
	 * @return 論理削除の場合 true を返す。
	 */
	private boolean isLogicalDelete() {
		return (this.invisible == null && this.mm.isLogicalDelete())
				|| Boolean.TRUE.equals(this.invisible);
	}

	/**
	 * 検索処理
	 *
	 * @param id ID
	 * @param model 汎用モデル
	 * @return レコードが不在の場合 false を返す。
	 */
	public boolean find(final Number id, final UniModel model) {
		if (id != null) {
			Serializable obj = super.find(this.mm.getDaoClass(), NumberUtil.toLong(id));
			if (obj != null && (!isLogicalDelete() || !isDeleted(obj))) {
				ModelUtil.setBeanValue(model, obj);
				return true;
			}
		}
		ModelUtil.noValueFields(model, this.mm.getDaoClass());
		return false;
	}

	/**
	 * 検索処理
	 *
	 * @param id ID
	 * @param ver バージョン
	 * @return 他で更新されていた場合 null を返す。
	 */
	private Serializable findWithLoc(final Number id, final Number ver) {
		try {
			Serializable obj = super.findWithLock(this.mm.getDaoClass(), NumberUtil.toLong(id));
			if (obj != null && (!isLogicalDelete() || !isDeleted(obj))) {
				if (super.isUpdatable(obj, ver)) {
					return obj;
				}
			}
		} catch (DaoLockException ex) {
			if (!ex.isNoWait()) {
				throw ex;
			}
		}
		return null;
	}

	/**
	 * 作成処理
	 *
	 * @param model 汎用モデル
	 * @return 他で更新されていた場合 false を返す。
	 */
	public boolean insert(final UniModel model) {
		Serializable obj = Factory.create(this.mm.getDaoClass());
		ModelUtil.setModelValue(obj, model);
		if (isLogicalDelete()) {
			removeDeleted(obj);
		}

		try {
			super.insert(obj);
			return true;
		} catch (DaoConstraintException ex) {
			if (!ex.isNoWait()) {
				throw ex;
			}
			return false;
		}
	}

	/**
	 * 更新処理
	 *
	 * @param id ID
	 * @param ver バージョン
	 * @param model 汎用モデル
	 * @return 他で更新されていた場合 false を返す。
	 */
	public boolean update(final Number id, final Number ver, final UniModel model) {
		Serializable obj = findWithLoc(id, ver);
		if (obj != null) {
			ModelUtil.setModelValue(obj, model);
			try {
				return super.update(obj);
			} catch (DaoConstraintException ex) {
				if (!ex.isNoWait()) {
					throw ex;
				}
				LogManager.getLogger().warn(ex.getMessage());
			}
		}
		return false;
	}

	/**
	 * 削除処理
	 *
	 * @param id ID
	 * @param ver バージョン
	 * @return 他で更新されていた場合 false を返す。
	 */
	public boolean delete(final Number id, final Number ver) {
		Serializable obj = findWithLoc(id, ver);
		if (obj != null) {
			try {
				// 論理削除
				if (isLogicalDelete()) {
					return super.invisible(obj);
				}
				return super.delete(obj);
			} catch (DaoConstraintException ex) {
				if (!ex.isNoWait()) {
					throw ex;
				}
				LogManager.getLogger().warn(ex.getMessage());
			}
		}
		return false;
	}

	/**
	 * 削除判断
	 *
	 * @param obj オブジェクト
	 * @return 削除済みの場合 true を返す。
	 */
	private boolean isDeleted(final Object obj) {
		return Integer.valueOf(0).equals(DaoUtil.getValue(obj, this.mm.getVersionName()));
	}

	/**
	 * 論理削除物理削除処理
	 * @param obj 削除値保持オブジェクト
	 */
	private void removeDeleted(final Serializable obj) {
		try (Dao dao = JdbcSource.getDao(super.getSchema())) {
			IndexWork iw = new IndexWork(this.mm.getTable());
			dao.doWork(iw);
			String[] keys = iw.getUniqueIndex();
			if (keys != null && 0 != keys.length
					&& (1 < keys.length || !"Id".equalsIgnoreCase(keys[0]))) {
				dao.execute("DELETE FROM " + this.mm.getTable()
					+ " WHERE " + getWhere(keys), getParams(obj, keys));
			}
		}
	}

	/**
	 * 削除用条件句取得
	 * @param keys キー文字列配列
	 * @return 削除用条件句
	 */
	private String getWhere(final String[] keys) {
		StringBuilder sb = new StringBuilder();
		int i = 1;
		for (final String key : keys) {
			if (0 < sb.length()) {
				sb.append(" AND ");
			}
			sb.append(key).append(" = :").append(i++);
		}
		sb.append(" AND ").append(this.mm.getVersionName()).append(" = 0 ");
		return sb.toString();
	}

	/**
	 * 削除用パラメタ取得
	 * @param obj 削除値保持オブジェクト
	 * @param keys キー文字列配列
	 * @return 削除用パラメタ
	 */
	private Object[] getParams(final Serializable obj, final String[] keys) {
		List<Object> ret = new ArrayList<>();
		for (final String key : keys) {
			ret.add(DaoUtil.getValue(obj, CamelCase.convert(key)));
		}
		return ret.toArray(new Object[ret.size()]);
	}

	/**
	 * ユニークインデックス取得
	 * @author Tadashi Nakayama
	 */
	private static final class IndexWork extends HibernateJdbcWork {
		/** テーブル名 */
		private String table;
		/** インデックス */
		private String[] index;

		/**
		 * コンストラクタ
		 * @param val テーブル名
		 */
		public IndexWork(final String val) {
			this.table = val;
		}

		/**
		 * ユニークインデックス取得
		 * @return ユニークインデックス
		 */
		public String[] getUniqueIndex() {
			return ArrayUtil.copyOf(this.index);
		}

		/**
		 * @see common.db.dao.hibernate.HibernateJdbcWork#execute(java.sql.Connection)
		 */
		@Override
		public void execute(final Connection conn) throws SQLException {
			List<String> list = getUniqueIndexList(conn);
			if (list.size() == 1) {
				this.index = list.get(0).split(",");
				return;
			}

			for (final String val : list) {
				if (!"id".equalsIgnoreCase(val)) {
					this.index = val.split(",");
					return;
				}
			}

			this.index = new String[0];
		}

		/**
		 * ユニークインデックス取得
		 * @param conn コネクション
		 * @return ユニークインデックスリスト
		 * @throws SQLException SQL例外
		 */
		private List<String> getUniqueIndexList(final Connection conn) throws SQLException {
			List<String> list = new ArrayList<>();
			DatabaseMetaData dmd = conn.getMetaData();
			try (ResultSet rs = dmd.getIndexInfo(null, null, this.table, true, false)) {
				StringBuilder sb = new StringBuilder();
				String name = null;
				while (rs.next()) {
//					ResultSetMetaData rsmd = rs.getMetaData();
//					for (int i = 0; i < rsmd.getColumnCount(); i++) {
//						System.out.println(
//								rsmd.getColumnName(i + 1) + ":" + rs.getObject(i + 1));
//					}
					if (name != null && !name.equalsIgnoreCase(rs.getString("INDEX_NAME"))) {
						list.add(sb.toString());
						sb = new StringBuilder();
					}
					if (0 < sb.length()) {
						sb.append(",");
					}
					sb.append(rs.getString("COLUMN_NAME"));
					name = rs.getString("INDEX_NAME");
				}
				if (0 < sb.length()) {
					list.add(sb.toString());
				}
			}
			return list;
		}
	}
}
